﻿namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
    using System;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Diagnostics.Contracts;

    public class AssertExTestBase
    {
        #region Protected Methods
        protected void TestForFailure(Expression<Func<bool>> assertion, string expectedMessage)
        {
            Contract.Requires(assertion != null);

            try
            {
                AssertEx.That(assertion);
            }
            catch (AssertFailedException ex)
            {
                Assert.AreEqual(expectedMessage, ex.Message);
            }
        }

        protected void TestForInconclusive(Expression<Func<bool>> assertion, string expectedMessage)
        {
            Contract.Requires(assertion != null);

            try
            {
                AssertEx.Inconclusive(assertion);
            }
            catch (AssertInconclusiveException ex)
            {
                Assert.AreEqual(expectedMessage, ex.Message);
            }
        }
        #endregion Protected Methods
    }

    [TestClass]
    public class When_testing_boolean_expressions : AssertExTestBase
    {
        [TestMethod]
        public void True_boolean_expressions_are_OK()
        {
            bool trueValue = true;
            bool falseValue = false;
            new Expression<Func<bool>>[]
            {
                () => trueValue,
                () => !falseValue,
                () => trueValue == true,
                () => falseValue == false,
                () => trueValue != false,
                () => falseValue != true
            }
            .Run(assertion => AssertEx.That(assertion));
        }

        [TestMethod]
        public void False_boolean_expressions_fail()
        {
            bool trueValue = true;
            bool falseValue = false;

            // Value was false assertions.
            new Expression<Func<bool>>[]
            {
                () => falseValue,
                () => falseValue == true,
                () => falseValue != false,
            }
            .Run(assertion => this.TestForFailure(assertion, "AssertEx failed. 'falseValue' was false."));

            // Value was true assertions.
            new Expression<Func<bool>>[]
            {
                () => trueValue == false,
                () => trueValue != true,
                () => !trueValue,
            }
            .Run(assertion => this.TestForFailure(assertion, "AssertEx failed. 'trueValue' was true."));
        }
    }

    [TestClass]
    public class When_testing_equality_expressions : AssertExTestBase
    {
        [TestMethod]
        public void Equal_values_are_OK()
        {
            int number = 1;
            AssertEx.That(() => number == 1);
        }

        [TestMethod]
        public void Not_equal_values_fails()
        {
            int number = 1;
            this.TestForFailure(
                () => number == 0,
                "AssertEx failed. 'number' was expected to be equal to be <0> but was <1>.");
        }

        [TestMethod]
        public void Equal_enums_are_OK()
        {
            AttributeTargets value = AttributeTargets.Class;
            AssertEx.That(() => value == AttributeTargets.Class);
        }

        [TestMethod]
        public void Not_equal_enums_fails()
        {
            AttributeTargets value = AttributeTargets.Class;
            this.TestForFailure(
                () => value == AttributeTargets.Assembly,
                "AssertEx failed. 'value' was expected to be equal to be <Assembly> but was <Class>.");
        }
    }

    [TestClass]
    public class When_testing_inequality_expressions : AssertExTestBase
    {
        [TestMethod]
        public void Not_equal_values_is_OK()
        {
            int number = 5;
            AssertEx.That(() => number != 1);
        }

        [TestMethod]
        public void Equal_values_fails()
        {
            int number = 1;
            this.TestForFailure(
                () => number != 1,
                "AssertEx failed. 'number' was not expected to be equal to be <1> but was.");
        }

        [TestMethod]
        public void Not_equal_enums_is_OK()
        {
            AttributeTargets value = AttributeTargets.Class;
            AssertEx.That(() => value != AttributeTargets.Assembly);
        }

        [TestMethod]
        public void Equal_enums_fails()
        {
            AttributeTargets value = AttributeTargets.Class;
            this.TestForFailure(
                () => value != AttributeTargets.Class,
                "AssertEx failed. 'value' was not expected to be equal to be <Class> but was.");
        }
    }

    [TestClass]
    public class When_testing_null_expressions : AssertExTestBase
    {
        [TestMethod]
        public void Null_is_OK()
        {
            object item = null;
            AssertEx.That(() => item == null);
        }

        [TestMethod]
        public void Non_null_fails()
        {
            object item = new object();
            this.TestForFailure(
                () => item == null,
                "AssertEx failed. 'item' was expected to be null but isn't.");
        }
    }

    [TestClass]
    public class When_testing_non_null_expressions : AssertExTestBase
    {
        [TestMethod]
        public void Non_null_is_OK()
        {
            object item = new object();
            AssertEx.That(() => item != null);
        }

        [TestMethod]
        public void Null_fails()
        {
            object item = null;
            this.TestForFailure(
                () => item != null,
                "AssertEx failed. 'item' is null but was expected not to be null.");
        }
    }

    [TestClass]
    public class When_testing_a_type_is : AssertExTestBase
    {
        [TestMethod]
        public void Same_type_is_OK()
        {
            object item = decimal.Zero;
            AssertEx.That(() => item is decimal);
        }

        [TestMethod]
        public void Different_type_fails()
        {
            object item = string.Empty;
            this.TestForFailure(
                () => item is decimal,
                "AssertEx failed. 'item' was expected to be of type <System.Decimal> but was of type <System.String>.");
        }

        [TestMethod]
        public void Null_instance_fails()
        {
            object item = null;
            this.TestForFailure(
                () => item is decimal,
                "AssertEx failed. 'item' was expected to be of type <System.Decimal> but was null.");
        }
    }

    [TestClass]
    public class When_testing_a_type_is_not : AssertExTestBase
    {
        [TestMethod]
        public void Different_type_is_OK()
        {
            object item = decimal.Zero;
            AssertEx.That(() => !(item is string));
        }

        [TestMethod]
        public void Same_type_fails()
        {
            object item = string.Empty;
            this.TestForFailure(
                () => !(item is string),
                "AssertEx failed. 'item' was expected not to be of type <System.String> but was.");
        }

        [TestMethod]
        public void Subclass_type_fails()
        {
            FileStyleUriParser item = new FileStyleUriParser();
            this.TestForFailure(
                () => !(item is UriParser),
                "AssertEx failed. 'item' was expected not to be of type <System.UriParser> but was of the subclass type <System.FileStyleUriParser>.");
        }

        [TestMethod]
        public void Null_instance_fails()
        {
            object item = null;
            this.TestForFailure(
                () => !(item is string),
                "AssertEx failed. 'item' was expected not to be of type <System.String> but was null.");
        }
    }

    [TestClass]
    public class When_testing_for_reference_equals : AssertExTestBase
    {
        [TestMethod]
        public void Same_instance_is_OK()
        {
            object o = new object();
            AssertEx.That(() => object.ReferenceEquals(o, o));
        }

        [TestMethod]
        public void Same_instance_in_different_variables_is_OK()
        {
            object o1 = new object();
            object o2 = o1;
            AssertEx.That(() => object.ReferenceEquals(o1, o2));
        }

        [TestMethod]
        public void Different_instances_fails()
        {
            object o1 = new object();
            object o2 = new object();
            this.TestForFailure(
                () => object.ReferenceEquals(o1, o2),
                "AssertEx failed. 'o1' and 'o2' were expected to be the same instance but were not.");
        }
    }

    [TestClass]
    public class When_testing_for_not_reference_equal : AssertExTestBase
    {
        [TestMethod]
        public void Not_same_is_OK()
        {
            object o1 = new object();
            object o2 = new object();
            AssertEx.That(() => !object.ReferenceEquals(o1, o2));
        }

        [TestMethod]
        public void Same_instance_fails()
        {
            object o = new object();
            this.TestForFailure(
                () => !object.ReferenceEquals(o, o),
                "AssertEx failed. 'o' and 'o' were expected to be different but were the same instance.");
        }

        [TestMethod]
        public void Same_instance_different_variables_fails()
        {
            object o1 = new object();
            object o2 = o1;
            this.TestForFailure(
                () => !object.ReferenceEquals(o1, o2),
                "AssertEx failed. 'o1' and 'o2' were expected to be different but were the same instance.");
        }
    }

    [TestClass]
    public class When_testing_string_EndsWith : AssertExTestBase
    {
        [TestMethod]
        public void Null_target_fails()
        {
            this.TestForFailure(
                () => ((string)null).EndsWith("abc"),
                "AssertEx failed. Constant '(null)' is null.");
        }

        [TestMethod]
        public void Null_value_is_an_exception()
        {
            AssertEx.Throws<ArgumentNullException>(
                () =>
                {
                    AssertEx.That(() => "abc".EndsWith(null));
                });
        }

        [TestMethod]
        public void Same_end_is_OK()
        {
            AssertEx.That(() => "defghi".EndsWith("ghi"));
        }

        [TestMethod]
        public void StringComparison_and_same_end_is_OK()
        {
            string value = "defghi";
            AssertEx.That(() => value.EndsWith("GHI", StringComparison.OrdinalIgnoreCase));
        }

        [TestMethod]
        public void Three_parameters_and_same_end_is_OK()
        {
            string value = "defghi";
            AssertEx.That(() => value.EndsWith("GHI", true, CultureInfo.InvariantCulture));
        }

        [TestMethod]
        public void Different_end_fails()
        {
            string value = "def";
            this.TestForFailure(
                () => value.EndsWith("abc"),
                "AssertEx failed. 'value' with value <def> doesn't end with <abc>.");
        }

        [TestMethod]
        public void StringComparison_and_different_end_fails()
        {
            string value = "def";
            this.TestForFailure(
                () => value.EndsWith("abc", StringComparison.OrdinalIgnoreCase),
                "AssertEx failed. 'value' with value <def> doesn't end with <abc>.");
        }

        [TestMethod]
        public void Three_parameters_and_different_end_fails()
        {
            string value = "def";
            this.TestForFailure(
                () => value.EndsWith("abc", true, CultureInfo.InvariantCulture),
                "AssertEx failed. 'value' with value <def> doesn't end with <abc>.");
        }
    }

    [TestClass]
    public class When_testing_string_StartsWith : AssertExTestBase
    {
        [TestMethod]
        public void Null_target_fails()
        {
            this.TestForFailure(
                () => ((string)null).StartsWith("abc"),
                "AssertEx failed. Constant '(null)' is null.");
        }

        [TestMethod]
        public void Null_value_is_an_exception()
        {
            AssertEx.Throws<ArgumentNullException>(
                () =>
                {
                    AssertEx.That(() => "abc".StartsWith(null));
                });
        }

        [TestMethod]
        public void Same_start_is_OK()
        {
            AssertEx.That(() => "defghi".StartsWith("def"));
        }

        [TestMethod]
        public void StringComparison_and_same_start_is_OK()
        {
            string value = "defghi";
            AssertEx.That(() => value.StartsWith("DEF", StringComparison.OrdinalIgnoreCase));
        }

        [TestMethod]
        public void Three_parameters_and_same_start_is_OK()
        {
            string value = "defghi";
            AssertEx.That(() => value.StartsWith("DEF", true, CultureInfo.InvariantCulture));
        }

        [TestMethod]
        public void Different_starts_fails()
        {
            string value = "def";
            this.TestForFailure(
                () => value.StartsWith("abc"),
                "AssertEx failed. 'value' with value <def> doesn't start with <abc>.");
        }

        [TestMethod]
        public void StringComparison_and_different_start_fails()
        {
            string value = "def";
            this.TestForFailure(
                () => value.StartsWith("abc", StringComparison.OrdinalIgnoreCase),
                "AssertEx failed. 'value' with value <def> doesn't start with <abc>.");
        }

        [TestMethod]
        public void Three_parameters_and_different_start_fails()
        {
            string value = "def";
            this.TestForFailure(
                () => value.StartsWith("abc", true, CultureInfo.InvariantCulture),
                "AssertEx failed. 'value' with value <def> doesn't start with <abc>.");
        }
    }

    [TestClass]
    public class When_building_assert_message
    {
        [TestMethod]
        public void Failure_message_is_used()
        {
            try
            {
                AssertEx.That(() => false, "Message");
            }
            catch (AssertFailedException ex)
            {
                Assert.AreEqual("AssertEx failed. Constant 'False' was false.\r\nMessage", ex.Message);
            }
        }

        [TestMethod]
        public void Failure_formatted_message_is_used()
        {
            try
            {
                AssertEx.That(() => false, "Message {0}", "sub message");
            }
            catch (AssertFailedException ex)
            {
                Assert.AreEqual("AssertEx failed. Constant 'False' was false.\r\nMessage sub message", ex.Message);
            }
        }

        [TestMethod]
        public void Failure_unformatted_message_with_format_characters_is_used()
        {
            try
            {
                // The { character is a formatting character so will throw an exception because of invalid formatting if
                // this is used in string.Format(...). If there are no arguments then the string should just be passed through.
                AssertEx.That(() => false, "Message { testing is good }");
            }
            catch (AssertFailedException ex)
            {
                Assert.AreEqual("AssertEx failed. Constant 'False' was false.\r\nMessage { testing is good }", ex.Message);
            }
        }
    }

    [TestClass]
    public class When_pretty_formatting_messages : AssertExTestBase
    {
        [TestMethod]
        public void Constant_expression()
        {
            this.TestForFailure(
                () => false,
                "AssertEx failed. Constant 'False' was false.");
        }

        [TestMethod]
        public void Enum_constants()
        {
            AttributeTargets value = AttributeTargets.Class;
            this.TestForFailure(
                () => value == AttributeTargets.Assembly,
                "AssertEx failed. 'value' was expected to be equal to be <Assembly> but was <Class>.");
        }

        [TestMethod]
        public void Hoisted_local_variable()
        {
            int item = 5;
            this.TestForFailure(
                () => item == 4,
                "AssertEx failed. 'item' was expected to be equal to be <4> but was <5>.");
        }

        [TestMethod]
        public void Property()
        {
            string item = "abc";
            this.TestForFailure(
                () => item.Length == 99,
                "AssertEx failed. 'Length' was expected to be equal to be <99> but was <3>.");
        }

        [TestMethod]
        public void Method_call_no_parameters()
        {
            string item = "abc";
            this.TestForFailure(
                () => item.ToString() == "XXX",
                "AssertEx failed. Method call 'ToString()' was expected to be equal to be <XXX> but was <abc>.");
        }

        [TestMethod]
        public void Method_call_with_parameters()
        {
            string item = "abc";
            this.TestForFailure(
                () => item.ToString(CultureInfo.CurrentCulture) == "XXX",
                "AssertEx failed. Method call 'ToString(provider)' was expected to be equal to be <XXX> but was <abc>.");
        }
    }

    [TestClass]
    public class When_testing_Inconclusive : AssertExTestBase
    {
        [TestMethod]
        public void True_does_not_fail()
        {
            AssertEx.Inconclusive(() => true);
        }

        [TestMethod]
        public void True_and_message_does_not_fail()
        {
            AssertEx.Inconclusive(() => true, "MESSAGE");
        }

        [TestMethod]
        public void False_fails()
        {
            this.TestForInconclusive(
                () => false,
                "AssertEx inconclusive. Constant 'False' was false.");
        }
    }
    
    [TestClass]
    public class When_debugging_assert_methods_are_stepped_through
    {
        [TestMethod]
        public void Class_should_have_DebuggerStepThrough_attribute()
        {
            AssertEx.That(() => typeof(AssertEx).GetCustomAttributes(typeof(DebuggerStepThroughAttribute), false).Any());
        }
    }
}